/**
 * ***************************************************************************** Copyright (c) 2003,
 * 2006 Subclipse project and others. All rights reserved. This program and the accompanying
 * materials are made available under the terms of the Eclipse Public License v1.0 which accompanies
 * this distribution, and is available at http://www.eclipse.org/legal/epl-v10.html
 *
 * <p>Contributors: Subclipse project committers - initial API and implementation
 * ****************************************************************************
 */
package org.tigris.subversion.subclipse.core.repo;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.team.core.TeamException;
import org.osgi.service.prefs.BackingStoreException;
import org.tigris.subversion.subclipse.core.ISVNRepositoryLocation;
import org.tigris.subversion.subclipse.core.Policy;
import org.tigris.subversion.subclipse.core.SVNException;
import org.tigris.subversion.subclipse.core.SVNProviderPlugin;
import org.tigris.subversion.subclipse.core.SVNStatus;
import org.tigris.subversion.subclipse.core.util.Util;
import org.tigris.subversion.svnclientadapter.SVNUrl;

/** The list of known repositories */
public class SVNRepositories {
  private Map<String, ISVNRepositoryLocation> repositories =
      new HashMap<String, ISVNRepositoryLocation>();
  private static final String REPOSITORIES_STATE_FILE = ".svnProviderState"; // $NON-NLS-1$

  // version numbers for the state file
  private static final int REPOSITORIES_STATE_FILE_VERSION_1 = 1;
  private static final int REPOSITORIES_STATE_FILE_VERSION_2 = 2;
  private static final int REPOSITORIES_STATE_FILE_VERSION_3 = 3;

  /*
   * Add the repository location to the cached locations
   */
  private void addToRepositoriesCache(ISVNRepositoryLocation repository) {
    repositories.put(repository.getLocation(), repository);
    SVNProviderPlugin.getPlugin().getRepositoryResourcesManager().repositoryAdded(repository);
  }

  /*
   * Remove the repository location from the cached locations
   */
  private void removeFromRepositoriesCache(ISVNRepositoryLocation repository) {
    if (repositories.remove(repository.getLocation()) != null) {
      SVNProviderPlugin.getPlugin().getRepositoryResourcesManager().repositoryRemoved(repository);
    }
  }

  /**
   * Add the repository to the receiver's list of known repositories or update it. Doing this will
   * enable password caching accross platform invokations.
   */
  public void addOrUpdateRepository(ISVNRepositoryLocation repository) throws SVNException {
    // Check the cache for an equivalent instance and if there is one, just update the cache
    SVNRepositoryLocation existingLocation =
        (SVNRepositoryLocation) repositories.get(repository.getLocation());
    if (existingLocation != null) {
      SVNProviderPlugin.getPlugin().getRepositoryResourcesManager().repositoryModified(repository);
      ((SVNRepositoryLocation) repository).updateCache();
    } else {
      // Cache the password and register the repository location
      addToRepositoriesCache(repository);
      ((SVNRepositoryLocation) repository).updateCache();
    }
    saveState();
  }

  /**
   * Dispose of the repository location
   *
   * <p>Removes any cached information about the repository such as a remembered password.
   */
  public void disposeRepository(ISVNRepositoryLocation repository) throws SVNException {
    ((SVNRepositoryLocation) repository).dispose();
    removeFromRepositoriesCache(repository);
  }

  /** Return a list of the know repository locations */
  public ISVNRepositoryLocation[] getKnownRepositories(IProgressMonitor monitor) {
    IProgressMonitor progress = Policy.monitorFor(monitor);
    IEclipsePreferences prefs = (IEclipsePreferences) SVNRepositoryLocation.getParentPreferences();
    try {
      String[] keys = prefs.childrenNames();
      progress.beginTask(Policy.bind("SVNRepositories.refresh"), keys.length); // $NON-NLS-1$
      for (String key : keys) {
        progress.worked(1);
        try {
          IEclipsePreferences node = (IEclipsePreferences) prefs.node(key);
          String location = node.get(SVNRepositoryLocation.PREF_LOCATION, null);
          if (location != null && !exactMatchExists(location)) {
            ISVNRepositoryLocation repos = SVNRepositoryLocation.fromString(location);
            try {
              repos.validateConnection(new NullProgressMonitor());
            } catch (SVNException swallow) {
            }
            addToRepositoriesCache(repos);
          }
        } catch (SVNException e) {
          // Log and continue
          SVNProviderPlugin.log(e);
        }
      }
    } catch (BackingStoreException e) {
      // Log and continue (although all repos will be missing)
      SVNProviderPlugin.log(SVNException.wrapException(e));
    }
    progress.done();
    return (ISVNRepositoryLocation[])
        repositories.values().toArray(new ISVNRepositoryLocation[repositories.size()]);
  }

  public void refreshRepositoriesFolders(IProgressMonitor monitor) {
    ISVNRepositoryLocation[] repositories = getKnownRepositories(monitor);
    for (ISVNRepositoryLocation repository : repositories) {
      repository.refreshRootFolder();
    }
  }

  /**
   * Create a repository instance from the given properties. The supported properties are:
   *
   * <p>user The username for the connection (optional) password The password used for the
   * connection (optional) url The url where the repository resides rootUrl The root url of the
   * subversion repository (optional)
   *
   * <p>The created instance is not known by the provider and it's user information is not cached.
   * The purpose of the created location is to allow connection validation before adding the
   * location to the provider.
   *
   * <p>This method will throw a SVNException if the location for the given configuration already
   * exists.
   */
  public ISVNRepositoryLocation createRepository(Properties configuration) throws SVNException {
    // Create a new repository location
    SVNRepositoryLocation location = SVNRepositoryLocation.fromProperties(configuration);

    // Check the cache for an equivalent instance and if there is one, throw an exception
    SVNRepositoryLocation existingLocation =
        (SVNRepositoryLocation) repositories.get(location.getLocation());
    if (existingLocation != null) {
      throw new SVNException(
          new SVNStatus(SVNStatus.ERROR, Policy.bind("SVNProvider.alreadyExists"))); // $NON-NLS-1$
    }

    return location;
  }

  public ISVNRepositoryLocation getRepository(String location) throws SVNException {
    return getRepository(location, true);
  }

  /**
   * Get the repository instance which matches the given String. The format of the String is an url
   */
  public ISVNRepositoryLocation getRepository(String location, boolean useRootUrl)
      throws SVNException {

    Set<String> keys = repositories.keySet();
    for (String url : keys) {
      if (url.equals(location) || location.indexOf(url + "/") != -1) {
        return (ISVNRepositoryLocation) repositories.get(url);
      }
    }

    // If we haven't found a matching repository yet, check to see if the default
    // port is redundantly specified in the location.  If it is, check the known
    // repositories again to see if there is a match for the location with the
    // default port stripped out (normalizedLocation).
    String normalizedLocation = getNormalizedLocation(location);
    if (!normalizedLocation.equals(location)) {
      for (String url : keys) {
        if (url.equals(normalizedLocation) || normalizedLocation.indexOf(url + "/") != -1) {
          return (ISVNRepositoryLocation) repositories.get(url);
        }
      }
    }

    // else we couldn't find it, fall through to adding new repo.

    ISVNRepositoryLocation repository =
        SVNRepositoryLocation.fromString(location, false, useRootUrl);
    addToRepositoriesCache(repository);

    return repository;
  }

  // If the default port is redundantly specified in the location, strip it out.
  private String getNormalizedLocation(String location) {
    try {
      URL url = new URL(location);
      if (url.getPort() == -1
          || url.getDefaultPort() == -1
          || url.getPort() != url.getDefaultPort()) return location;
      url = new URL(url.getProtocol(), url.getHost(), -1, url.getPath());
      return url.toString();
    } catch (MalformedURLException e) {
      return location;
    }
  }

  /** load the state of the plugin, ie the repositories locations */
  private void loadState() {
    try {
      IPath pluginStateLocation =
          SVNProviderPlugin.getPlugin().getStateLocation().append(REPOSITORIES_STATE_FILE);
      File file = pluginStateLocation.toFile();
      if (file.exists()) {
        try {
          DataInputStream dis = new DataInputStream(new FileInputStream(file));
          readState(dis);
          dis.close();
        } catch (IOException e) {
          throw new TeamException(
              new Status(
                  Status.ERROR,
                  SVNProviderPlugin.ID,
                  TeamException.UNABLE,
                  Policy.bind("SVNProvider.ioException"),
                  e)); //$NON-NLS-1$
        }
      }
    } catch (TeamException e) {
      Util.logError(Policy.bind("SVNProvider.errorLoading"), e); // $NON-NLS-1$
    }
  }

  /** Save the state of the plugin, ie the repositories locations */
  private void saveState() {
    try {
      IPath pluginStateLocation = SVNProviderPlugin.getPlugin().getStateLocation();
      File tempFile =
          pluginStateLocation.append(REPOSITORIES_STATE_FILE + ".tmp").toFile(); // $NON-NLS-1$
      File stateFile = pluginStateLocation.append(REPOSITORIES_STATE_FILE).toFile();
      try {
        DataOutputStream dos = new DataOutputStream(new FileOutputStream(tempFile));
        writeState(dos);
        dos.close();
        if (stateFile.exists()) {
          stateFile.delete();
        }
        boolean renamed = tempFile.renameTo(stateFile);
        if (!renamed) {
          throw new TeamException(
              new Status(
                  Status.ERROR,
                  SVNProviderPlugin.ID,
                  TeamException.UNABLE,
                  Policy.bind("SVNProvider.rename", tempFile.getAbsolutePath()),
                  null)); //$NON-NLS-1$
        }
      } catch (IOException e) {
        throw new TeamException(
            new Status(
                Status.ERROR,
                SVNProviderPlugin.ID,
                TeamException.UNABLE,
                Policy.bind("SVNProvider.save", stateFile.getAbsolutePath()),
                e)); //$NON-NLS-1$
      }
    } catch (TeamException e) {
      Util.logError(Policy.bind("SVNProvider.errorSaving"), e); // $NON-NLS-1$
    }
  }

  /**
   * read the state of the plugin, ie the repositories locations
   *
   * @param dis
   * @throws IOException
   * @throws SVNException
   */
  private void readState(DataInputStream dis) throws IOException, SVNException {
    int version = dis.readInt();

    if ((version < REPOSITORIES_STATE_FILE_VERSION_1)
        || (version > REPOSITORIES_STATE_FILE_VERSION_3)) {
      Util.logError(
          Policy.bind("SVNProviderPlugin.unknownStateFileVersion", new Integer(version).toString()),
          null); //$NON-NLS-1$
      return;
    }

    int count = dis.readInt();
    for (int i = 0; i < count; i++) {
      ISVNRepositoryLocation root = SVNRepositoryLocation.fromString(dis.readUTF());
      addToRepositoriesCache(root);
      if (version >= REPOSITORIES_STATE_FILE_VERSION_2) {
        String label = dis.readUTF();
        if (!label.equals("")) {
          root.setLabel(label);
        }
      }
      if (version >= REPOSITORIES_STATE_FILE_VERSION_3) {
        String repositoryRoot = dis.readUTF();
        if (!repositoryRoot.equals("")) {
          root.setRepositoryRoot(new SVNUrl(repositoryRoot));
        }
      }
    }
  }

  /**
   * write the state of the plugin ie the repositories locations
   *
   * @param dos
   * @throws IOException
   */
  private void writeState(DataOutputStream dos) throws IOException {
    // Write the repositories
    dos.writeInt(REPOSITORIES_STATE_FILE_VERSION_3);
    // Write out the repos
    Collection<ISVNRepositoryLocation> repos = repositories.values();
    dos.writeInt(repos.size());
    for (ISVNRepositoryLocation reposLocation : repos) {
      SVNRepositoryLocation root = (SVNRepositoryLocation) reposLocation;
      dos.writeUTF(root.getLocation());
      if (root.getLabel() == null) {
        dos.writeUTF("");
      } else {
        dos.writeUTF(root.getLabel());
      }
      if (root.getRepositoryRoot() == null) {
        dos.writeUTF("");
      } else {
        dos.writeUTF(root.getRepositoryRoot().toString());
      }
    }
    dos.flush();
    dos.close();
  }

  public void startup() {
    loadState();
  }

  public void shutdown() {
    saveState();
  }

  /**
   * Answer whether the provided repository location is known by the provider or not. The location
   * string corresponds to the Strin returned by ISVNRepositoryLocation#getLocation()
   */
  public boolean isKnownRepository(String location, boolean requireExactMatch) {
    Set<String> keys = repositories.keySet();
    for (String checkLocation : keys) {
      if (!requireExactMatch && location.indexOf(checkLocation) != -1) {
        return true;
      }
      if (location.equals(checkLocation)) return true;
    }
    return false;
  }

  /**
   * Answer whether the provided repository location already has an exact match location The
   * location string corresponds to the Strin returned by ISVNRepositoryLocation#getLocation()
   */
  public boolean exactMatchExists(String location) {
    Set<String> keys = repositories.keySet();
    for (String url : keys) {
      if (url.equals(location)) {
        return true;
      }
    }
    return false;
  }
}
